ECS Exec のログ記録はタスクロールで行われるため注意しよう
こんにちは!AWS事業本部コンサルティング部のたかくに(@takakuni_)です。
ECS Exec のログ記録について、権限周りで詰まった点があったのでご紹介したいと思います。
3行まとめ
- ECS Exec ではタスクロールを利用してログの配信を行う
- ECS クラスターの logging プロパティが
DEFAULT
の制御であってもタスクロールでログの配信を行う DEFAULT
の場合、awslogs
ログドライバーが使われるがタスクロールを利用してログの配信を行う
ECS Exec
ECS Exec とは、 Amazon ECS ( Amazon EC2 インスタンスまたは AWS Fargate ) で実行されているコンテナに対してコマンドを実行できる機能です。
稼働しているコンテナに対して、直接デバックを行えるため大変便利な機能です。
ログに関して
ECS Exec ではコマンドの実行履歴をログとして出力できます。ログの設定は、 ECS Exec を実行するタスクに関連づいた ECS クラスターで行います。
以下の3種類に設定できます。
NONE
:ログを記録しないDEFAULT
:awslogs
ログドライバーで送信するlogConfiguration
を null にした場合はログを配信しない
OVERRIDE
: 指定したCloudWatch Logs , Amazon S3 またはその両方に送信するよう設定する
当初考えていたこと
上記のDEFAULT
設定を利用すれば、タスクロールに権限を付与しなくていいと思っていました。
理由は以下のドキュメントにある通り、 AWS Fargate 環境下で awslogs
ログドライバーを利用する場合は、タスク実行ロールがログの配信を担うためです。
以下に示しているのは、タスク実行 IAM ロールの一般的なユースケースです。
タスクは AWS Fargate または外部インスタンスでホストされています…
- Amazon ECR プライベートリポジトリからコンテナイメージをプルします。
- awslogs ログドライバーを使用して CloudWatch Logs にコンテナログを送信します。詳細については、「awslogs ログドライバーを使用する」を参照してください。
実際は、タスクロールを利用してログの配信を行う仕様だったため、動作を確認してみようと思います。
実際にやってみた
フォルダ構造は以下の通りです。必要に応じて適宜見たいファイルをクリックしてください。
├── ecs.tf
├── iam_policy_document
│ ├── assume_ecs_task.json
│ ├── iam_task_nginx.json
│ └── iam_task_nginx_mistake.json
├── providers.tf
├── task_definition
│ └── nginx.json
├── terraform.tfstate
├── terraform.tfstate.backup
└── vpc.tf
ecs.tf
######################################
# CloudWatch Logs Configuration
######################################
resource "aws_cloudwatch_log_group" "nginx" {
name = "/ecs/${local.prefix}/nginx"
retention_in_days = 1
tags = {
Name = "/ecs/${local.prefix}/nginx/"
}
}
resource "aws_cloudwatch_log_group" "nginx_exec" {
name = "/ecs/${local.prefix}/nginx-ecs-exec"
retention_in_days = 1
tags = {
Name = "/ecs/${local.prefix}/nginx-ecs-exec"
}
}
######################################
# ECS Cluster Configuration
######################################
resource "aws_ecs_cluster" "cluster" {
name = "${local.prefix}-cluster"
configuration {
execute_command_configuration {
# logging = "OVERRIDE"
logging = "DEFAULT"
log_configuration {
cloud_watch_log_group_name = aws_cloudwatch_log_group.nginx_exec.name
}
}
}
tags = {
Name = "${local.prefix}-cluster"
}
}
######################################
# ECS Task Configuration
######################################
resource "aws_iam_role" "task_exec" {
name = "${local.prefix}-task-exec-role"
assume_role_policy = file("${path.module}/iam_policy_document/assume_ecs_task.json")
tags = {
Name = "${local.prefix}-task-exec-role"
}
}
resource "aws_iam_role_policy_attachment" "task_exec" {
role = aws_iam_role.task_exec.name
policy_arn = "arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy"
}
resource "aws_iam_role" "task_nginx" {
name = "${local.prefix}-task-nginx-role"
assume_role_policy = file("${path.module}/iam_policy_document/assume_ecs_task.json")
tags = {
Name = "${local.prefix}-task-nginx-role"
}
}
resource "aws_iam_policy" "task_nginx" {
name = "${local.prefix}-task-nginx-policy"
policy = file("${path.module}/iam_policy_document/iam_task_nginx.json")
tags = {
Name = "${local.prefix}-task-nginx-policy"
}
}
resource "aws_iam_role_policy_attachment" "task_nginx" {
role = aws_iam_role.task_nginx.name
policy_arn = aws_iam_policy.task_nginx.arn
}
data "aws_ecr_repository" "amazon_linux" {
name = "ecr-public/amazonlinux/amazonlinux"
}
resource "aws_ecs_task_definition" "nginx" {
family = "${local.prefix}-nginx-td"
requires_compatibilities = ["FARGATE"]
network_mode = "awsvpc"
memory = 512
cpu = 256
execution_role_arn = aws_iam_role.task_exec.arn
task_role_arn = aws_iam_role.task_nginx.arn
container_definitions = templatefile("${path.module}/task_definition/nginx.json", {
image_url = "public.ecr.aws/nginx/nginx:latest",
log_group_name = aws_cloudwatch_log_group.nginx.name,
region = data.aws_region.current.name,
log_stream_prefix = "sample-nginx"
})
lifecycle {
ignore_changes = [
container_definitions
]
}
}
######################################
# ECS Task Configuration
######################################
resource "aws_security_group" "nginx" {
name = "${local.prefix}-nginx-sg"
description = "${local.prefix}-nginx-sg"
vpc_id = aws_vpc.vpc.id
ingress {
description = "My IP"
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = ["${chomp(data.http.ifconfig.response_body)}/32"]
}
egress {
description = "All Connection"
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
tags = {
Name = "${local.prefix}-nginx-sg"
}
}
resource "aws_ecs_service" "nginx" {
name = "${local.prefix}-nginx-service"
cluster = aws_ecs_cluster.cluster.id
task_definition = aws_ecs_task_definition.nginx.arn
launch_type = "FARGATE"
desired_count = 1
enable_execute_command = true
network_configuration {
security_groups = [aws_security_group.nginx.id]
subnets = [aws_subnet.public_a.id]
assign_public_ip = true
}
tags = {
Name = "${local.prefix}-nginx-service"
}
}
iam_policy_document/
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "",
"Effect": "Allow",
"Principal": {
"Service": "ecs-tasks.amazonaws.com"
},
"Action": "sts:AssumeRole"
}
]
}
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"ssmmessages:CreateControlChannel",
"ssmmessages:CreateDataChannel",
"ssmmessages:OpenControlChannel",
"ssmmessages:OpenDataChannel"
],
"Resource": "*"
},
{
"Effect": "Allow",
"Action": "logs:DescribeLogGroups",
"Resource": "*"
},
{
"Effect": "Allow",
"Action": [
"logs:CreateLogStream",
"logs:DescribeLogStreams",
"logs:PutLogEvents"
],
"Resource": "*"
}
]
}
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"ssmmessages:CreateControlChannel",
"ssmmessages:CreateDataChannel",
"ssmmessages:OpenControlChannel",
"ssmmessages:OpenDataChannel"
],
"Resource": "*"
}
]
}
providers.tf
######################################
# Provider Configuration
######################################
terraform {
required_version = "~> 1.3.0"
required_providers {
aws = {
source = "hashicorp/aws"
version = "4.46.0"
}
}
}
provider "aws" {
region = "ap-northeast-1"
}
######################################
# Data Configuration
######################################
data "aws_caller_identity" "self" {}
data "aws_region" "current" {}
data "http" "ifconfig" {
url = "http://ipv4.icanhazip.com/"
}
######################################
# Environment Variables Configuration
######################################
variable "system" {
type = string
default = "nginx"
}
variable "env" {
type = string
default = "sample"
}
locals {
prefix = "${var.system}-${var.env}"
}
task_definition
[
{
"name": "nginx",
"image": "${image_url}",
"essential": true,
"linuxParameters": {
"initProcessEnabled": true
},
"portMappings": [
{
"containerPort": 80,
"hostPort": 80,
"protocol": "tcp"
}
],
"logConfiguration": {
"logDriver": "awslogs",
"options": {
"awslogs-group": "${log_group_name}",
"awslogs-region": "${region}",
"awslogs-stream-prefix": "${log_stream_prefix}"
}
}
}
]
vpc.tf
######################################
# VPC Configuration
######################################
resource "aws_vpc" "vpc" {
cidr_block = "10.0.0.0/16"
enable_dns_hostnames = true
enable_dns_support = true
tags = {
Name = "${local.prefix}-vpc"
}
}
######################################
# Public Subnet Configuration
######################################
resource "aws_internet_gateway" "igw" {
vpc_id = aws_vpc.vpc.id
tags = {
Name = "${local.prefix}-igw"
}
}
resource "aws_route_table" "public" {
vpc_id = aws_vpc.vpc.id
tags = {
Name = "${local.prefix}-public-rtb"
}
}
resource "aws_route" "public_igw" {
route_table_id = aws_route_table.public.id
destination_cidr_block = "0.0.0.0/0"
gateway_id = aws_internet_gateway.igw.id
}
resource "aws_subnet" "public_a" {
vpc_id = aws_vpc.vpc.id
availability_zone = "${data.aws_region.current.name}a"
cidr_block = "10.0.0.0/24"
tags = {
Name = "${local.prefix}-public-a-subnet"
}
}
resource "aws_route_table_association" "public_a" {
subnet_id = aws_subnet.public_a.id
route_table_id = aws_route_table.public.id
}
あっている方
まずはタスクロールに権限が付与されている方をデプロイしてみます。
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"ssmmessages:CreateControlChannel",
"ssmmessages:CreateDataChannel",
"ssmmessages:OpenControlChannel",
"ssmmessages:OpenDataChannel"
],
"Resource": "*"
},
{
"Effect": "Allow",
"Action": "logs:DescribeLogGroups",
"Resource": "*"
},
{
"Effect": "Allow",
"Action": [
"logs:CreateLogStream",
"logs:DescribeLogStreams",
"logs:PutLogEvents"
],
"Resource": "*"
}
]
}
ECS Exec を実行して、適当にコマンドを打ってみます。
[cloudshell-user@ip-10-6-77-133 ~]$ aws ecs execute-command --cluster nginx-sample-cluster --container nginx --command /bin/bash --interactive --task arn:aws:ecs:ap-northeast-1:111111111111:task/nginx-sample-cluster/a9112a0a96ed445cbc93a90807e34e37
The Session Manager plugin was installed successfully. Use the AWS CLI to start a session.
Starting session with SessionId: ecs-execute-command-0d656f7d108ce1d22
root@ip-10-0-0-143:/# echo "hello world"
hello world
root@ip-10-0-0-143:/# ping google.com
bash: ping: command not found
root@ip-10-0-0-143:/# aws
bash: aws: command not found
root@ip-10-0-0-143:/# curl localhost
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
<style>
html { color-scheme: light dark; }
body { width: 35em; margin: 0 auto;
font-family: Tahoma, Verdana, Arial, sans-serif; }
</style>
</head>
<body>
<h1>Welcome to nginx!</h1>
<p>If you see this page, the nginx web server is successfully installed and
working. Further configuration is required.</p>
<p>For online documentation and support please refer to
[nginx.org](http://nginx.org/).<br/>
Commercial support is available at
[nginx.com](http://nginx.com/).</p>
<p><em>Thank you for using nginx.</em></p>
</body>
</html>
root@ip-10-0-0-143:/# exit
exit
Exiting session with sessionId: ecs-execute-command-0d656f7d108ce1d22.
CloudWatch Logs を確認するとログストリームが作成されていることが確認できました。
間違っている方
続いて、タスクロールの IAM ポリシーを変更します。
~~~~~~~~~~~~~~~~~~(省略)~~~~~~~~~~~~~~~~~~~~~~~~
resource "aws_iam_policy" "task_nginx" {
name = "${local.prefix}-task-nginx-policy"
+ policy = file("${path.module}/iam_policy_document/iam_task_nginx_mistake.json")
- policy = file("${path.module}/iam_policy_document/iam_task_nginx.json")
tags = {
Name = "${local.prefix}-task-nginx-policy"
}
}
~~~~~~~~~~~~~~~~~~(省略)~~~~~~~~~~~~~~~~~~~~~~~~
変更後の IAM ポリシーは以下の通りです。
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"ssmmessages:CreateControlChannel",
"ssmmessages:CreateDataChannel",
"ssmmessages:OpenControlChannel",
"ssmmessages:OpenDataChannel"
],
"Resource": "*"
}
]
}
こちらも適当に ECS Exec でコマンドを打ってみます。
[cloudshell-user@ip-10-6-77-133 ~]$ aws ecs execute-command --cluster nginx-sample-cluster --container nginx --command /bin/bash --interactive --task arn:aws:ecs:ap-northeast-1:622809842341:task/nginx-sample-cluster/a9112a0a96ed445cbc93a90807e34e37
The Session Manager plugin was installed successfully. Use the AWS CLI to start a session.
Starting session with SessionId: ecs-execute-command-0289df90ce62e19cc
root@ip-10-0-0-143:/# echo "hello world"
hello world
root@ip-10-0-0-143:/# ping google.com
bash: ping: command not found
root@ip-10-0-0-143:/# aws
bash: aws: command not found
root@ip-10-0-0-143:/# curl localhost
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
<style>
html { color-scheme: light dark; }
body { width: 35em; margin: 0 auto;
font-family: Tahoma, Verdana, Arial, sans-serif; }
</style>
</head>
<body>
<h1>Welcome to nginx!</h1>
<p>If you see this page, the nginx web server is successfully installed and
working. Further configuration is required.</p>
<p>For online documentation and support please refer to
[nginx.org](http://nginx.org/).<br/>
Commercial support is available at
[nginx.com](http://nginx.com/).</p>
<p><em>Thank you for using nginx.</em></p>
</body>
</html>
root@ip-10-0-0-143:/# exit
exit
Exiting session with sessionId: ecs-execute-command-0289df90ce62e19cc.
CloudWatch Logs を確認しましたが、残念ながらログストリームの数は増えていませんでした。
CloudTrail を確認してみる
「ログストリームが作成できませんでした。つまり、タスクロールが権限不足です。」はあまりにも強引なので、 CloudTrail で確認してみました。
すると、直近のイベント履歴から「AccessDenied」とエラーが返されたイベントが見つかりました。
ちなみにユーザー名の「a9112a0a96ed445cbc93a90807e34e37」は ECS Exec を利用したタスク ID です。
もう少しエラーの中身を確認してみます。すると、errorMessage
からタスクロールを利用して、ログ配信が行われていることがわかります。
よって、ECS Exec のログ配信はタスクロールが使われていることがわかりました。
{
"eventVersion": "1.08",
"userIdentity": {
"type": "AssumedRole",
"principalId": "AROAXXXXXXXXXXXXXXXXX:a9112a0a96ed445cbc93a90807e34e37",
"arn": "arn:aws:sts::111111111111:assumed-role/nginx-sample-task-nginx-role/a9112a0a96ed445cbc93a90807e34e37",
"accountId": "111111111111",
"accessKeyId": "ASIAXXXXXXXXXXXXXXXX",
"sessionContext": {
"sessionIssuer": {
"type": "Role",
"principalId": "AROAXXXXXXXXXXXXXXXXX",
"arn": "arn:aws:iam::111111111111:role/nginx-sample-task-nginx-role",
"accountId": "111111111111",
"userName": "nginx-sample-task-nginx-role"
},
"webIdFederationData": {},
"attributes": {
"creationDate": "2023-02-11T06:04:12Z",
"mfaAuthenticated": "false"
}
}
},
"eventTime": "2023-02-11T06:35:04Z",
"eventSource": "logs.amazonaws.com",
"eventName": "CreateLogStream",
"awsRegion": "ap-northeast-1",
"sourceIPAddress": "13.XXX.XXX.XXX",
"userAgent": "aws-sdk-go/1.41.4 (go1.18.3; linux; amd64) exec-env/AWS_ECS_FARGATE amazon-ssm-agent/3.1.1732.0",
"errorCode": "AccessDenied",
"errorMessage": "User: arn:aws:sts::111111111111:assumed-role/nginx-sample-task-nginx-role/a9112a0a96ed445cbc93a90807e34e37 is not authorized to perform: logs:CreateLogStream on resource: arn:aws:logs:ap-northeast-1:111111111111:log-group:/ecs/nginx-sample/nginx:log-stream:ecs-execute-command-0289df90ce62e19cc because no identity-based policy allows the logs:CreateLogStream action",
"requestParameters": null,
"responseElements": null,
"requestID": "846035a4-1b10-4776-8b9e-a44b058d416d",
"eventID": "95a42f28-d6de-406c-91df-e24980696995",
"readOnly": false,
"eventType": "AwsApiCall",
"managementEvent": true,
"recipientAccountId": "111111111111",
"eventCategory": "Management",
"tlsDetails": {
"tlsVersion": "TLSv1.2",
"cipherSuite": "ECDHE-RSA-AES128-GCM-SHA256",
"clientProvidedHostHeader": "logs.ap-northeast-1.amazonaws.com"
}
}
まとめ
以上、「ECS Exec のログ記録はタスクロールで行われるため注意しよう」でした。
awslogs
ログドライバーは、標準出力/標準エラー出力では、タスク実行ロール。 ECS Exec ではタスクロールを利用します。
地味なはまりどころですが、どなたかの参考になれば幸いです。(ちなみにOVERRIDE
ログ設定もタスクロールを使用すると明記されいているため、総じて ECS Exec はタスクロールを使用すると覚えていいかと思いました。)
以上、AWS事業本部コンサルティング部のたかくに(@takakuni_)でした!